feat(options): a reachable collect heartbeat, and name the pointer-width route boundary - #32
Merged
Conversation
…dth route boundary The Kairos RTOS reported one alloc+free stepping 16% across 512 bytes on a 32-bit ESP32-S3, absent on a 64-bit host, and concluded it points at `prim::fixed`. It does not. `SMALL_SIZE_MAX` is `128 * size_of::<usize>()`: 1,024 on their host, 512 on the device, where it coincides with `SMALL_OBJ_SIZE_MAX`. Their refutation of that constant was run on a machine where it sits at 1,024. Moving only the pointer width, with `prim::windows` in both arms (ABBA, 50k ops/arm, min of 40 blocks): x86-64 +0.1% at 512 vs 513, i686 +8.6%, control under 1% on both. Mechanism, counted rather than timed: per 100,000 pairs with one block live the `direct[]` route carves/extends/retires 195 pages and the bin route 0, while `generic`/op is 1.0000 on both -- so the slow path is not the difference. 195 is 100,000/512, the GENERIC_COLLECT sweep at the small profile. That period is a real trade (churn against capacity decay) and bare metal could not move it: `options::set` is a no-op under ONE_REGION and the option's doc told firmwares to change a built-in default that had no cfg. Adds `--cfg ra_generic_collect`; the default does not move, because 10,000 starved this profile once already. Also adds `prim::fixed::shape_of(size) -> Shape` (page bytes, dedicated segments, direct_route) so the route and page kind are a const answer instead of a silicon discovery -- the observability the report asked for, which `region_stats()` cannot give because it reports over extents. Not established, and withdrawn rather than quoted: how much of the 16% the churn accounts for. Raising the heartbeat took churn to a measured zero, but that run's timing control flipped +8.0% -> +0.6%, so the instrument was deciding it. Plan closed: docs/plans/finished/fixed-prim-small-step.md section 8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The new pointer-width test carried `assert_ne!(SMALL_SIZE_MAX, SMALL_OBJ_SIZE_MAX)` on its 64-bit branch. That is a fact about the GEOMETRY, not the pointer, and it is false under `ra_segment_size="256k"`: an 8 KiB slice puts SMALL_OBJ_SIZE_MAX at 1024, which is where the 64-bit route top already is. Green at the default slice, red on the 256k rung -- the exact defect shape this repo's own notes describe, committed in the commit that documented it. Derived from the arithmetic now, so it says something at every geometry. The coincidence is also the interesting half: where the two constants land on the same byte, one step hides two boundaries and a sweep cannot separate them, which is what made the reporting device confusing in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tracing collect_inner's reclaim gives the churn a face and one unexpected column: every reclaimed page carries capacity=1. `page_extend` links a 4 KiB payload bound, so a 512-byte class should return 8 blocks and a 1024-byte class 4. A page holding one block is exhausted by the next allocation, which is the missing half of why `generic` reads exactly 1.0000 per op. The trace also pins the route boundary exactly: bins 20/21/24 (the direct[] sizes at SMALL_SIZE_MAX=1024) churn ~200 times per 100k pairs, bins 25 and 28 just above it churn once. Recorded, not chased: it points at page_extend/page_fresh, which is a hot-path change needing the instruction-count battery rather than a session's tail. It is a better lead than the heartbeat knob, because it would close the step from the other side with no default moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes
docs/plans/fixed-prim-small-step.md(now infinished/, §8), filed by the Kairos RTOS.The correction is the finding
They measured a 16% step at 512 bytes on a 32-bit ESP32-S3, could not reproduce it on a 64-bit host, and concluded it points at
prim::fixed. It does not.SMALL_SIZE_MAXis128 * size_of::<usize>()— 1,024 on their host, 512 on the device, where it coincides withSMALL_OBJ_SIZE_MAX. Their refutation of that constant was run on a machine where the suspect is not at the scene. Moving only the pointer width,prim::windowsin both arms:ABBA-interleaved, 50,000 ops/arm, min of 40 blocks. The control resolves under 1%.
Mechanism, counted not timed
Per 100,000 pairs, one block live:
generic/opdirect[](<= SMALL_SIZE_MAX)Both take the slow path every op, so that is not it.
100_000 / 512 = 195is theGENERIC_COLLECTsweep. The boundary is the route, not the page kind: on 64-bit, 513–1024 are medium pages that still churn.What ships
--cfg ra_generic_collect="64" | "4096" | "65536". The period is a genuine trade and bare metal could not move it:options::setis a no-op underONE_REGION, and the option's own doc told firmwares to change a built-in default that had no cfg. Default unchanged — 10,000 starved this profile once already (168 → 8 blocks, 22,533/50,000 nulls).prim::fixed::shape_of(size) -> Shape— page bytes, dedicated segments,direct_route;const, geometry-derived. The observability §5 asked for, whichregion_stats()cannot give because it reports over extents.SMALL_SIZE_MAX == 128 * size_of::<usize>()and both width-specific values.Two of their §5 gaps were already reachable:
alloc::usable_sizeandalloc::stats()(which is the slow-path counter they wanted). Both live inalloc, notprim::fixed, so a seam re-exporting the fixed-region API in onepub usecannot see them — same shape as thePrimErrorgap closed in 2.1.0.Not established
How much of the 16% the churn accounts for. Raising the heartbeat took churn to a measured zero, but that run's timing control flipped +8.0% → +0.6%, so it is withdrawn rather than quoted. The device is the right box;
--cfg ra_generic_collect="65536"against their own sweep is the one-line experiment, andstats().pages_freshneeds no quiet core.Gates
20 suites at both profiles, gate-selftest 11/11, wasm 20,168 (baseline 20,169), clippy 0.
rusty_allocbuilds clean fori686-pc-windows-msvc; onlygenerator-0.8.9(aloomdev-dep) blocks running the harness there.🤖 Generated with Claude Code